﻿' Nathan Moschkin's universal scalable control template for .NET Framework 4.0+.
' Scalable scrolling container control with custom paint

Option Explicit On

Imports System
Imports System.IO
Imports System.Threading
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Windows.Forms
Imports System.ComponentModel

<Serializable()> _
Public Class VBUCTemplate

#Region "Public Events"

    ''' <summary>
    ''' Custom event handler arguments for the user-drawn control template.
    ''' </summary>
    ''' <remarks></remarks>
    Public Class OControlEventArgs
        Inherits EventArgs

#Region "Private Variables"
        Private m_CanCancel As Boolean = False
        Private m_Cancel As Boolean = False
        Private m_Cancelled As Boolean = False

        Private m_Location As System.Drawing.Point = Nothing
        Private m_Size As System.Drawing.Point = Nothing

        Private m_NewImage As System.Drawing.Image = Nothing
        Private m_OldImage As System.Drawing.Image = Nothing

        Private m_OldValue As Object = Nothing
        Private m_NewValue As Object = Nothing

#End Region

#Region "Read-Only Properties"

        ''' <summary>
        ''' Returns the rectangle of the control within the parent.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Bounds As Rectangle
            Get
                Return New Rectangle(m_Location, m_Size)
            End Get
        End Property

        ''' <summary>
        ''' Generic tag for previous object.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property OldValue As Object
            Get
                Return m_OldValue
            End Get
        End Property

        ''' <summary>
        ''' Generic tag for new object.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property NewValue As Object
            Get
                Return m_NewValue
            End Get
        End Property

        ''' <summary>
        ''' If applicable, the old image contained in the Image property.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property OldIamge As System.Drawing.Image
            Get
                Return m_OldImage
            End Get
        End Property

        ''' <summary>
        ''' If applicable, the new image to be set to the Image property.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property NewIamge As System.Drawing.Image
            Get
                Return m_NewImage
            End Get
        End Property

        ''' <summary>
        ''' Indicates whether or not an event can be cancelled.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property CanCancel As Boolean
            Get
                Return m_CanCancel
            End Get
        End Property


        ''' <summary>
        ''' Indicates whether or not an event has been cancelled.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public ReadOnly Property Cancelled As Boolean
            Get
                Return m_Cancelled
            End Get
        End Property

#End Region

#Region "Friend-Settable Properties"

        ''' <summary>
        ''' If applicable, indicates the new Size related to an event.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property Size As System.Drawing.Size
            Get
                Return m_Size
            End Get
            Friend Set(value As System.Drawing.Size)
                m_Size = value
            End Set
        End Property

        ''' <summary>
        ''' If applicable, indicates the new Location related to an event.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property Location As System.Drawing.Point
            Get
                Return m_Location
            End Get
            Friend Set(value As System.Drawing.Point)
                m_Location = value
            End Set
        End Property

#End Region

#Region "Public Properties"

        ''' <summary>
        ''' Get or set a value indicating that an event is to be cancelled by the caller.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Property Cancel As Boolean
            Get
                Return m_Cancel
            End Get
            Set(value As Boolean)

                If m_CanCancel = False Then
                    Throw New ArgumentException("Property cannot be set for this instance.", "Cancel")
                End If

                m_Cancel = value
                If value Then m_Cancelled = True
            End Set
        End Property

#End Region

#Region "Constructors"

        Friend Sub New(rect As Rectangle, ByVal canCancel As Boolean)

            m_Location = New System.Drawing.Point(rect.X, rect.Y)
            m_Size = New System.Drawing.Size(rect.Width, rect.Height)
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(loc As System.Drawing.Point, size As System.Drawing.Size, Optional ByVal canCancel As Boolean = False)

            m_Location = New System.Drawing.Point(loc.X, loc.Y)
            m_Size = New System.Drawing.Size(size.Width, size.Height)
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(size As System.Drawing.Size, Optional ByVal canCancel As Boolean = False)

            m_Size = New System.Drawing.Size(size.Width, size.Height)
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(loc As System.Drawing.Point, Optional ByVal canCancel As Boolean = False)

            m_Location = New System.Drawing.Point(loc.X, loc.Y)
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(oldImage As System.Drawing.Image, newImage As System.Drawing.Image, Optional ByVal canCancel As Boolean = False)
            m_OldImage = oldImage
            m_NewImage = newImage
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(oldValue As Double, newValue As Double, Optional ByVal canCancel As Boolean = False)
            m_OldValue = oldValue
            m_NewValue = newValue
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(oldValue As String, newValue As String, Optional ByVal canCancel As Boolean = False)
            m_OldValue = oldValue
            m_NewValue = newValue
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(oldValue As Integer, newValue As Integer, Optional ByVal canCancel As Boolean = False)
            m_OldValue = oldValue
            m_NewValue = newValue
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(oldValue As Object, newValue As Object, Optional ByVal canCancel As Boolean = False)
            m_OldValue = oldValue
            m_NewValue = newValue
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(canCancel As Boolean, ByVal didCancel As Boolean)
            m_Cancelled = didCancel
            m_CanCancel = canCancel
        End Sub

        Friend Sub New(canCancel As Boolean)
            m_CanCancel = canCancel
        End Sub

        Friend Sub New()

        End Sub

#End Region

    End Class

    Public Event ImageChanging(sender As Object, e As OControlEventArgs)
    Public Event ImageChanged(sender As Object, e As OControlEventArgs)

    Public Event CanvasRedrawn(sender As Object, e As OControlEventArgs)

    Public Event ScaleChanging(sender As Object, e As OControlEventArgs)
    Public Event ScaleChanged(sender As Object, e As OControlEventArgs)

    Public Event SelectionChanging(sender As Object, e As OControlEventArgs)
    Public Event SelectionChanged(sender As Object, e As OControlEventArgs)

    Public Event MoveBegin(sender As Object, e As OControlEventArgs)
    Public Event MoveEnd(sender As Object, e As OControlEventArgs)

    Public Event SizeBegin(sender As Object, e As OControlEventArgs)
    Public Event SizeEnd(sender As Object, e As OControlEventArgs)

    Public Event DrawBegin(sender As Object, e As OControlEventArgs)
    Public Event DrawEnd(sender As Object, e As OControlEventArgs)

    Public Event EditBegin(sender As Object, e As OControlEventArgs)
    Public Event EditEnd(sender As Object, e As OControlEventArgs)

#End Region

#Region "Private Non-Property Variables"

    Private mdpix As Single
    Private mdpiy As Single

    Private mdpmx As Single
    Private mdpmy As Single

    Private mLastScale As Single = 1.0#
    Private mPaintPic As System.Drawing.Image = Nothing
    Private mNeedRedraw As Boolean = True

#End Region

#Region "Private Property Variables"

    Private m_Preview As System.Drawing.Image

    Private m_ImageRect As System.Drawing.Rectangle
    Private m_Image As System.Drawing.Image

    Private m_MaxScrollX As Integer = 0
    Private m_MaxScrollY As Integer = 0
    '' because that's the way I roll

    Private m_Scaling As Double = 1.0#

#End Region

#Region "Public Properties"

    <Browsable(False)> _
    Public ReadOnly Property ImageBounds As Rectangle
        Get
            Return m_ImageRect
        End Get
    End Property

    <Browsable(True), Description("The scale factor for the image and all the controls."), Category("Appearance")> _
    Public Property Scaling() As Double
        Get
            Return m_Scaling
        End Get
        Set(ByVal value As Double)
            Dim c As Boolean = False
            Dim e As OControlEventArgs, _
                ov As Double = m_Scaling

            If (value < 0.2#) Then value = 0.2#
            If (value > 10.0#) Then value = 10.0#

            value = System.Math.Round(value, 4)

            e = New OControlEventArgs(ov, value, True)
            If (e.Cancelled) Then Return

            If (value = m_Scaling) Then Return

            ScaleControls(True)

            m_Scaling = value
            mNeedRedraw = True

            CalculateDimensions()

            Me.Invalidate()

            e = New OControlEventArgs(ov, value, False)
            RaiseEvent ScaleChanged(Me, e)

        End Set
    End Property

    <Browsable(True), Description("The image to be loaded (or saved) in the background"), Category("Behavior")> _
    Public Property Image() As System.Drawing.Image
        Get
            Return m_Image
        End Get
        Set(ByVal value As System.Drawing.Image)
            Dim cancel As Boolean = False, _
                oi As System.Drawing.Image = m_Image

            Dim ptO As System.Drawing.Point = Me.AutoScrollOffset

            Dim e As New OControlEventArgs(oi, value, True)

            RaiseEvent ImageChanging(Me, e)
            If (e.Cancelled) Then Return

            m_Image = value
            Thread.Sleep(0)

            mNeedRedraw = True
            CalculateDimensions()
            Me.Invalidate()

            e = New OControlEventArgs(oi, value, False)
            Me.AutoScrollPosition = ptO

        End Set
    End Property
#End Region

#Region "Image File Handling"

    <Description("Load an image file and redraw the control.")> _
    Public Sub LoadFile(Optional ByVal fileName As String = "")

        If fileName <> "" Then
            If File.Exists(fileName) Then
                fileName = ""
            End If
        End If

        If (fileName = "") Then
            Dim dlg As New System.Windows.Forms.OpenFileDialog

            dlg.ShowDialog()
            If dlg.FileName = "" Then Exit Sub
            fileName = dlg.FileName
        End If

        If File.Exists(fileName) Then
            Me.Image = New System.Drawing.Bitmap(fileName)
        End If

    End Sub

#End Region

#Region "Scrolling"

    <Browsable(True), Description("Maximum Horizontal Scroll Value in Pixels."), Category("Design")> _
    Public Property MaxScrollX() As Integer
        Get
            If (m_MaxScrollX < Me.ClientSize.Width) Then Return Me.ClientSize.Width
            Return m_MaxScrollX
        End Get
        Set(ByVal value As Integer)
            m_MaxScrollX = value

            mNeedRedraw = True
            CalculateDimensions()
            Me.Invalidate()

        End Set
    End Property

    <Browsable(True), Description("Maximum Vertical Scroll Value in Pixels."), Category("Design")> _
    Public Property MaxScrollY() As Integer
        Get
            If (m_MaxScrollY < Me.ClientSize.Height) Then Return Me.ClientSize.Height
            Return m_MaxScrollY
        End Get
        Set(ByVal value As Integer)
            m_MaxScrollY = value

            mNeedRedraw = True
            CalculateDimensions()
            Me.Invalidate()

        End Set
    End Property

    ''' <summary>
    ''' Translates the visible box from the scroll values into virtual coordinates.
    ''' </summary>
    ''' <param name="subject">Optional subject for which to return virtual coordinates. Subject must expose Left and Top properties.</param>
    ''' <returns>A new System.Drawing.Rectangle containing the virtual coordinates.</returns>
    ''' <remarks></remarks>
    Public Function ScrollTranslate(Optional subject As Object = Nothing) As System.Drawing.Rectangle
        Dim x As Integer, _
            y As Integer, _
            cx As Integer, _
            cy As Integer

        Dim c As Object = subject

        If c Is Nothing Then
            x = Me.AutoScrollPosition.X
            y = Me.AutoScrollPosition.Y

            cx = Me.ClientSize.Width
            cy = Me.ClientSize.Height
        Else
            x = Me.AutoScrollPosition.X + c.Left
            y = Me.AutoScrollPosition.Y + c.Top

            cx = c.Width
            cy = c.Height
        End If

        Return New System.Drawing.Rectangle(x, y, cx, cy)

    End Function

    ''' <summary>
    ''' Calculate the scroll dimensions based upon the current image size and scaling factor.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub CalculateDimensions()

        Dim x As Integer, _
            y As Integer

        Dim tWidth As Integer = 0
        Dim tHeight As Integer = 0

        SuspendLayout()

        ' if there is no image, we have no reason to establish scrolling; turn off scrolling.
        If m_Image Is Nothing Then
            Me.AutoScrollMinSize = New System.Drawing.Size(0, 0)

            Me.HScroll = False
            Me.VScroll = False

            Me.AutoScroll = False

            ResumeLayout()

            Return
        End If

        tWidth = CInt(CSng(m_Image.Width) * m_Scaling)
        tHeight = CInt(CSng(m_Image.Height) * m_Scaling)

        If (tHeight > Me.ClientSize.Height) Then
            y = tHeight
            Me.VScroll = True
        Else
            y = 0
            Me.VScroll = False
        End If

        If (tWidth > Me.ClientSize.Width) Then
            x = tWidth
            Me.HScroll = True
        Else
            x = 0
            Me.HScroll = False
        End If

        ResumeLayout()

        Me.AutoScroll = False
        Me.AutoScroll = True

        Me.AutoScrollMinSize = New System.Drawing.Size(x, y)

        m_MaxScrollX = x
        m_MaxScrollY = y

    End Sub

    ''' <summary>
    ''' Calculate the absolute distance from the closest boundary of rc to pt.
    ''' </summary>
    ''' <param name="left">Left coordinate of point.</param>
    ''' <param name="top">Top coordinate of point.</param>
    ''' <param name="rc">Rectangle on which to base calculations.</param>
    ''' <returns>A new size structure representing the boundary distance.</returns>
    ''' <remarks></remarks>
    Public Overloads Function BoundDistance(left As Integer, top As Integer, rc As Rectangle) As System.Drawing.Size
        Return BoundDistance(New System.Drawing.Point(left, top), rc)
    End Function

    ''' <summary>
    ''' Calculate the absolute distance from the closest boundary of rc to pt.
    ''' </summary>
    ''' <param name="pt">Point to calculate.</param>
    ''' <param name="rc">Rectangle on which to base calculations.</param>
    ''' <returns>A new size structure representing the boundary distance.</returns>
    ''' <remarks></remarks>
    Public Overloads Function BoundDistance(pt As System.Drawing.Point, rc As System.Drawing.Rectangle) As System.Drawing.Size

        Dim x As Integer, _
            y As Integer

        If (pt.X <= rc.Left) Then
            x = rc.Left - pt.X
        ElseIf (pt.X > rc.Right) Then
            x = pt.X - rc.Right
        End If

        If (pt.Y <= rc.Top) Then
            y = rc.Top - pt.Y
        ElseIf (pt.Y > rc.Bottom) Then
            y = pt.Y - rc.Bottom
        End If

        Return New System.Drawing.Size(x, y)

    End Function

#End Region

#Region "Scaling Methods"


    ''' <summary>
    ''' Record the last scaling.
    ''' </summary>
    ''' <param name="value">Scaling to record.</param>
    ''' <remarks></remarks>
    Protected Sub SetLastScale(value As Single)
        mLastScale = value
    End Sub

    Protected Sub ScaleControls(Optional ByVal Record As Boolean = False)

        Static cRatios() As System.Drawing.Rectangle
        Static NeedScale As Boolean = False
        Static InScale As Boolean = False

        If InScale = True Then Return
        InScale = True

        Dim c As Object
        Dim i As Integer = 0

        Dim f As Single, _
            g As Single, _
            h As Single, _
            j As Single

        Dim c_x As Integer, _
            c_y As Integer
        If Record = True Then
            Erase cRatios

            ReDim cRatios(Me.Controls.Count - 1)

            For Each c In Me.Controls

                '' get the virtual position of the control
                c_x = c.Left + Me.HorizontalScroll.Value
                c_y = c.Top + Me.VerticalScroll.Value

                f = CSng((c_x - m_ImageRect.Left)) / CSng(m_ImageRect.Width)
                g = CSng((c_y - m_ImageRect.Top)) / CSng(m_ImageRect.Height)
                h = CSng(c.Width) / CSng(m_ImageRect.Width)
                j = CSng(c.Height) / CSng(m_ImageRect.Height)

                cRatios(i) = New System.Drawing.Rectangle(f, g, h, j)
                i += 1

            Next

            NeedScale = True

        ElseIf NeedScale = True Then

            For Each c In Me.Controls

                With cRatios(i)

                    f = CSng(m_ImageRect.Left) + (m_ImageRect.Width * .Left)
                    g = CSng(m_ImageRect.Top) + (m_ImageRect.Height * .Top)
                    h = (m_ImageRect.Width * .Width)
                    j = (m_ImageRect.Height * .Height)

                    f -= Me.HorizontalScroll.Value
                    g -= Me.VerticalScroll.Value

                    c.SuspendLayout()

                    c.SetBounds(f, g, h, j)

                    c.ResumeLayout()
                    c.Invalidate()

                End With

                i += 1
            Next

        End If

        InScale = False

    End Sub

#End Region

#Region "User Interactions"

    Private Sub Control_DoubleClick(sender As Object, e As System.EventArgs) Handles Me.DoubleClick

    End Sub

    Private Sub Control_KeyDown(sender As Object, e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown

    End Sub

    Private Sub Control_MouseDown(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseDown

    End Sub

    Private Sub Control_MouseMove(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove

    End Sub

    Private Sub Control_MouseUp(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseUp

    End Sub

    Protected Overrides Sub OnMouseWheel(e As System.Windows.Forms.MouseEventArgs)

        MyBase.OnMouseWheel(e)
    End Sub

    Private Sub Control_Resize(sender As Object, e As System.EventArgs) Handles Me.Resize

        If (Me.Parent Is Nothing) Then Exit Sub
        SuspendLayout()

        If Me.Width > Me.Parent.ClientSize.Width Then
            Me.Width = Me.Parent.ClientSize.Width
        End If
        If Me.Height > Me.Parent.ClientSize.Height Then
            Me.Height = Me.Parent.ClientSize.Height
        End If

        mNeedRedraw = True

        ResumeLayout()
        Me.Invalidate()
    End Sub
#End Region

#Region "Painting and Rendering"

    Public Overrides Sub Refresh()
        mNeedRedraw = True
        MyBase.Refresh()
    End Sub

    Public Sub RefreshPreview(Optional ByVal FullRefresh As Boolean = False)
        If FullRefresh Then
            mNeedRedraw = True
        End If

        InternalPaint()
    End Sub

    Protected Overridable Sub FullPaint()

        Dim g As System.Drawing.Graphics

        Dim x As Integer = 0, _
            y As Integer = 0

        Dim i As Integer = 0

        Dim tWidth As Integer = 0
        Dim tHeight As Integer = 0

        Dim iWidth As Integer = 0
        Dim iHeight As Integer = 0

        Dim rc As Rectangle
        Dim brw As New SolidBrush(Me.BackColor)

        If (m_Image Is Nothing) Then
            m_Image = New Bitmap(Me.ClientRectangle.Width, Me.ClientRectangle.Height, PixelFormat.Format32bppArgb)
            g = Graphics.FromImage(m_Image)
            g.Clear(Color.White)
            g.Dispose()
        End If

        iWidth = CInt(CSng(m_Image.Width) * m_Scaling)
        iHeight = CInt(CSng(m_Image.Height) * m_Scaling)

        tWidth = iWidth
        tHeight = iHeight

        If (Me.ClientRectangle.Width > tWidth) Then
            tWidth = Me.ClientRectangle.Width
        End If

        If (Me.ClientRectangle.Height > tHeight) Then
            tHeight = Me.ClientRectangle.Height
        End If

        rc = New System.Drawing.Rectangle(0, 0, tWidth, tHeight)

        mPaintPic = New System.Drawing.Bitmap(tWidth, tHeight)
        g = System.Drawing.Graphics.FromImage(mPaintPic)

        g.FillRectangle(brw, rc)
        brw.Dispose()

        x = (tWidth / 2) - (iWidth / 2)
        y = (tHeight / 2) - (iHeight / 2)

        m_ImageRect = New System.Drawing.Rectangle(x, y, iWidth, iHeight)

        g.DrawImage(m_Image, m_ImageRect)


    End Sub

    Private Painting As Boolean = False

    Protected Overrides Sub OnPaint(e As System.Windows.Forms.PaintEventArgs)
        If (Painting) Then Return
        Painting = True

        '' Do some painting
        InternalPaint(e.Graphics)

        '' Blt the bitmap
        e.Graphics.DrawImage(mPaintPic, New Rectangle(0, 0, Me.ClientRectangle.Width, Me.ClientRectangle.Height), New Rectangle(HorizontalScroll.Value, VerticalScroll.Value, Me.ClientRectangle.Width, Me.ClientRectangle.Height), GraphicsUnit.Pixel)
        Painting = False
    End Sub

    Protected Sub InternalPaint(Optional g As System.Drawing.Graphics = Nothing, Optional ByVal forPreview As Boolean = False)
        If (mNeedRedraw) Then
            FullPaint()
            RaiseEvent CanvasRedrawn(Me, New OControlEventArgs(False))

            mNeedRedraw = False
        End If
    End Sub

#End Region

#Region "Constructing"

    Sub New()

        ' Update window styles before initializing component
        SetStyle(ControlStyles.ResizeRedraw Or ControlStyles.AllPaintingInWmPaint Or ControlStyles.OptimizedDoubleBuffer Or ControlStyles.SupportsTransparentBackColor Or ControlStyles.UserPaint, True)
        UpdateStyles()

        ' initialize component
        InitializeComponent()

        Me.AutoScroll = True
        Me.BackColor = System.Drawing.Color.Transparent

        ' Get the screen dimensions
        Dim g As Graphics = Me.CreateGraphics

        ' Pixels/Inch
        mdpix = g.DpiX
        mdpiy = g.DpiY

        ' Pixels/Millimeter
        mdpmx = mdpix / 25.4
        mdpmy = mdpiy / 25.4

        g.Dispose()

    End Sub

    Protected Overrides Sub Finalize()
        MyBase.Finalize()
    End Sub

#End Region

End Class
